package dkbta.ontology.elements.subjectstat;

import java.util.Calendar;
import java.util.List;

import dkbta.ontology.Ontology;
import dkbta.ontology.elements.Element;
import dkbta.ontology.elements.ElementDef;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.loading.OntologyException;
import dkbta.ontology.values.PercentValues;
import dkbta.ontology.values.ValueCondition;
import dkbta.processing.ActionTimer;
import dkbta.processing.Patient;
import dkbta.util.time.TimeInterval;
import dkbta.util.time.TimeUnit;

public class SubjectStatDef extends ElementDef{
	/** The condition on which the subject-statistical abstraction is based */
	private final ValueCondition _condition;

	/**
	 * The percent of the time frame for which elements matching the conditions should
	 * exist
	 */
	private final PercentValues _percentRange;

	/** The time unit of the time frame */
	private final TimeUnit _timeUnit;

	/** The number of time units */
	private final int _numOfTimeUnits;

	/**
	 * Initializes a subject-statistical abstraction definition
	 * 
	 * @param name The element's name
	 * @param componentType The element's component type
	 * @param valueCondition The condition on which the subject-statistical abstraction is
	 *        based
	 * @param percentRange The percent of the time frame for which elements matching the
	 *        conditions should exist
	 * @param timeUnit The time unit of the time frame
	 * @param numOfTimeUnits The number of time units
	 */
	public SubjectStatDef(String name, String componentType,
		ValueCondition valueCondition, PercentValues percentRange, TimeUnit timeUnit,
		int numOfTimeUnits){
		super(ElementType.SUBJECTSTAT, name, componentType, null);
		_condition = valueCondition;
		_percentRange = percentRange;
		_timeUnit = timeUnit;
		_numOfTimeUnits = numOfTimeUnits;
	}

	@Override
	public void validate(Ontology ontology) throws OntologyException{
		_condition.validate(ontology);
		String elementName = _condition.getElementName();
		ElementType elementType = _condition.getElementType();
		_prerequisiteElements.add(ontology.getElement(elementType, elementName));
	}

	@Override
	public String completeToString(){
		StringBuilder sb = new StringBuilder(super.completeToString());
		sb.append("Condition: ").append(_condition).append("\r\n");
		sb.append("Percent Range: ").append(_percentRange).append("\r\n");
		sb.append("Time Frame: ").append(_numOfTimeUnits).append(" ").append(_timeUnit)
				.append("s\r\n");
		return sb.toString();
	}

	@Override
	protected void compute(Patient p){
		ActionTimer actionTimer = ActionTimer.getInstance();
		int statActionId = actionTimer.actionStart();

		ElementType elementType = _condition.getElementType();
		String elementName = _condition.getElementName();
		List<? extends Element> elements = p.getElements(elementType, elementName);

		if (!elements.isEmpty()){
			compute(p.getSubjectStats(_name), elements);

		}
		actionTimer.actionEnd(statActionId, -1, _name, _type,
			ActionTimer.Action.SUBJECT_STATISTICAL_ABSTRACTION_COMPUTATION);
	}

	private void compute(List<SubjectStat> subStats, List<? extends Element> elements){
		// Initializing the time frame according to the first element
		Calendar frameStartCal = Calendar.getInstance();
		frameStartCal.setLenient(false);
		Calendar frameEndCal = (Calendar)frameStartCal.clone();
		Element e = elements.get(0);
		TimeInterval ti = e.getTimeInterval();
		setTimeFrameAccordintToTime(frameStartCal, frameEndCal, ti.getStartTime());
		long frameStart = frameStartCal.getTimeInMillis();
		long frameEnd = frameEndCal.getTimeInMillis();
		double frameSize = frameEnd - frameStart;
		double percent = 0;

		final int elementCount = elements.size();
		for (int i = 0; i < elementCount; ++i){
			e = elements.get(i);
			ti = e.getTimeInterval();
			long eStartTime = ti.getStartTime();
			long eEndTime = ti.getEndTime();

			if (eStartTime <= frameEnd){ // The interval start is within the time frame
				// If the element matches the condition it's duration within
				// the time frame adds to the percentage
				if (_condition.checkCondition(e)){
					percent += calcPercentWithinFrame(frameEnd, frameSize, eStartTime,
						eEndTime);

					while (eEndTime > frameEnd){
						// The interval exceeds the frame
						createAbstractionIfPercentIsValid(subStats, frameStart, frameEnd,
							percent);

						// Advancing a time frame
						frameStartCal.setTimeInMillis(frameEnd);
						advanceTimeFrame(frameEndCal);
						frameStart = frameEnd;
						frameEnd = frameEndCal.getTimeInMillis();
						frameSize = frameEnd - frameStart;
						percent = 0;

						eStartTime = frameStart;
						percent += calcPercentWithinFrame(frameEnd, frameSize,
							eStartTime, eEndTime);
					}
				}
			}else{
				// The current element is not longer within the current time frame
				// so we attempt to create an abstraction from the current frame,
				// advance the frame and re-visit the element within the next frame

				createAbstractionIfPercentIsValid(subStats, frameStart, frameEnd, percent);

				// Setting the new frame according to the new interval
				setTimeFrameAccordintToTime(frameStartCal, frameEndCal, eStartTime);
				frameStart = frameStartCal.getTimeInMillis();
				frameEnd = frameEndCal.getTimeInMillis();
				frameSize = frameEnd - frameStart;
				percent = 0;

				--i; // Taking a step back to "re-do" this element
			}
		}

		createAbstractionIfPercentIsValid(subStats, frameStart, frameEnd, percent);
	}

	private double calcPercentWithinFrame(long frameEnd, double frameSize,
		long eStartTime, long eEndTime){
		return (Math.min(frameEnd, eEndTime) - eStartTime) / frameSize;
	}

	private void createAbstractionIfPercentIsValid(List<SubjectStat> subStats,
		long frameStart, long frameEnd, double percent){
		if (_percentRange.isValid(percent * 100)){
			subStats.add(new SubjectStat(_name, frameStart, frameEnd));
		}
	}

	private void setTimeFrameAccordintToTime(Calendar frameStart, Calendar frameEnd,
		long time){
		// Setting the start of the frame at the start time of the interval
		frameStart.setTimeInMillis(time);

		// "Truncating" the time frame (setting the smaller components to initial values)
		if (_timeUnit == TimeUnit.MONTH){
			frameStart.set(Calendar.DAY_OF_MONTH, 1);
		}else if (_timeUnit == TimeUnit.WEEK){
			frameStart.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
		}
		switch (_timeUnit){
			case MONTH:
			case WEEK:
			case DAY:
				frameStart.set(Calendar.HOUR_OF_DAY, 0);
			case HOUR:
				frameStart.set(Calendar.MILLISECOND, 0);
				frameStart.set(Calendar.SECOND, 0);
				frameStart.set(Calendar.MINUTE, 0);
				break;
			default:
				throw new RuntimeException("Invalid time unit: " + _timeUnit);
		}

		// Setting the end of the frame to be (start + timeUnit * numOfTimeUnits)
		frameEnd.setTimeInMillis(frameStart.getTimeInMillis());
		advanceTimeFrame(frameEnd);
	}

	/**
	 * Setting cal.time = cal.time + timeUnit * numOfTimeUnits
	 * 
	 * @param cal The calendar to be advanced
	 */
	private void advanceTimeFrame(Calendar cal){
		cal.add(_timeUnit.calendarTimeUnit, _numOfTimeUnits);
	}

	@Override
	public SubjectStat newInstance(long startTime, long endTime, String[] value){
		throw new UnsupportedOperationException(
				"Subject statistical abstractions aren't supposed to be instantiated this way!");
	}

}
